{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "# Открытый курс по машинному обучению\n",
    "<center>Автор: Борисов Артём (@amphyby)\n",
    "<center>\n",
    "## Знакомимся с Microsoft Cognitive Toolkit"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Немного общих слов\n",
    "\n",
    "Существует много Deep Learning фреймворков от различных именитых компаний. Часть из них хорошо известны в open source сообществе, часть - чуть менее известны. <img src=\"../../img/DLframeworks.png\">\n",
    "Суть проста: не TensorFlow единым сыт дата-сатанист. Этот материал посвящен ознакомлению читателя с DL фреймворком Microsoft Cognitive Toolkit (cntk в простонародье). Помимо того, что он лежит в open-source чуть менее, чем полностью, при расчетах на 2х и более GPU он на раз-два обходит Tensorflow по производительности. Поддерживается и развивается фреймворк подразделением Microsoft Research, а из примеров использования в проде первым приходит на ум поисковик Bing.\n",
    "\n",
    "Использовать cntk можно из powershell, c#, c++, и конечно же python (вроде еще из жавы, но это не точно). CNTK есть под Linux и Windows, для Mac OS на данный момент можно только docker-образ с CNTK посоветовать. Установка, при использовании только из python, сводится к элементарному:\n",
    "**pip install https://cntk.ai/PythonWheel/CPU-Only/cntk-2.4-cp36-cp36m-win_amd64.whl**\n",
    "Существует большой выбор разных сборок для версий py2/py3 и использования на CPU(intel)/GPU(nvidia)/GPU-1bit-quantized-SGD(для магистров черной магии)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "### Поехали!\n",
    "Знакомиться с CNTK будем на примере классификации классического (для примеров классификации изображений) датасета рукописных цифр MNIST. Cразу заимпортим, всё что обещает пригодиться в рамках блокнота:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import cntk\n",
    "\n",
    "cntk.__version__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "# Зададим желаемое целевое устройство для расчетов:\n",
    "if \"TEST_DEVICE\" in os.environ:\n",
    "    if os.environ[\"TEST_DEVICE\"] == \"cpu\":\n",
    "        cntk.device.try_set_default_device(cntk.device.cpu())\n",
    "#    else:\n",
    "#        cntk.device.try_set_default_device(cntk.device.gpu(0))\n",
    "# В моем случае это только процессор"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import gzip\n",
    "import shutil\n",
    "import struct\n",
    "import sys\n",
    "from urllib.request import urlretrieve\n",
    "\n",
    "import matplotlib.image as mpimg\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Опишем функции для загрузки самого датасета. Он состоит из 60000 тренировочных картинок и 10000 тестовых. Каждая картинка - черно-белая, размером 28x28px. Т.е. каждая запись датасета это список из (28*28=) 784элементов со значениями, соответствующими оттенкам серого (у нас их аж 256 - Э.Л.Джеймс бы обзавидовалась) плюс 1 значение метки."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def loadData(src, cimg):\n",
    "    print(\"Идёт загрузка \" + src)\n",
    "    gzfname, h = urlretrieve(src, \"./delete.me\")\n",
    "    print(\"Загрузка завершена.\")\n",
    "    try:\n",
    "        with gzip.open(gzfname) as gz:\n",
    "            n = struct.unpack(\"I\", gz.read(4))\n",
    "            # проверяем \"магическое\" число из первых байтов файла\n",
    "            if n[0] != 0x3080000:\n",
    "                raise Exception(\"Invalid file: unexpected magic number.\")\n",
    "            # считываем количество записей\n",
    "            n = struct.unpack(\">I\", gz.read(4))[0]\n",
    "            if n != cimg:\n",
    "                raise Exception(\"Invalid file: expected {0} entries.\".format(cimg))\n",
    "            crow = struct.unpack(\">I\", gz.read(4))[0]\n",
    "            ccol = struct.unpack(\">I\", gz.read(4))[0]\n",
    "            if crow != 28 or ccol != 28:\n",
    "                raise Exception(\"Invalid file: expected 28 rows/cols per image.\")\n",
    "            # считываем данные в один очень длинный список\n",
    "            res = np.fromstring(gz.read(cimg * crow * ccol), dtype=np.uint8)\n",
    "    finally:\n",
    "        os.remove(gzfname)\n",
    "    return res.reshape((cimg, crow * ccol))\n",
    "\n",
    "\n",
    "def loadLabels(src, cimg):\n",
    "    print(\"Идёт загрузка \" + src)\n",
    "    gzfname, h = urlretrieve(src, \"./delete.me\")\n",
    "    print(\"Загрузка завершена.\")\n",
    "    try:\n",
    "        with gzip.open(gzfname) as gz:\n",
    "            n = struct.unpack(\"I\", gz.read(4))\n",
    "            # проверяем \"магическое\" число из первых байтов файла\n",
    "            if n[0] != 0x1080000:\n",
    "                raise Exception(\"Invalid file: unexpected magic number.\")\n",
    "            # считываем количество записей\n",
    "            n = struct.unpack(\">I\", gz.read(4))\n",
    "            if n[0] != cimg:\n",
    "                raise Exception(\"Invalid file: expected {0} rows.\".format(cimg))\n",
    "            # считываем метки\n",
    "            res = np.fromstring(gz.read(cimg), dtype=np.uint8)\n",
    "    finally:\n",
    "        os.remove(gzfname)\n",
    "    return res.reshape((cimg, 1))\n",
    "\n",
    "\n",
    "def try_download(dataSrc, labelsSrc, cimg):\n",
    "    data = loadData(dataSrc, cimg)\n",
    "    labels = loadLabels(labelsSrc, cimg)\n",
    "    return np.hstack((data, labels))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Скачаем данные:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# URL-ы обучающей выборки\n",
    "url_train_image = \"http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz\"\n",
    "url_train_labels = \"http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz\"\n",
    "num_train_samples = 60000\n",
    "\n",
    "print(\"Загрузка обучающей выборки\")\n",
    "train = try_download(url_train_image, url_train_labels, num_train_samples)\n",
    "\n",
    "# URL-ы тестовой выборки\n",
    "url_test_image = \"http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz\"\n",
    "url_test_labels = \"http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz\"\n",
    "num_test_samples = 10000\n",
    "\n",
    "print(\"Загрузка тестовой выборки\")\n",
    "test = try_download(url_test_image, url_test_labels, num_test_samples)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проверим, что всё скачалось и в том формате, в котором мы ожидаем:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.shape(train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.shape(test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Полюбуемся на случайный экземпляр обучающей выборки:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sample_number = np.random.randint(60000)\n",
    "plt.imshow(train[sample_number, :-1].reshape(28, 28), cmap=\"gray_r\")\n",
    "plt.axis(\"off\")\n",
    "print(\"Метка: \", train[sample_number, -1])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Сохранение данных в формате совметимом с функционалом CNTK text reader\n",
    "def savetxt(filename, ndarray):\n",
    "    dir = os.path.dirname(filename)\n",
    "\n",
    "    if not os.path.exists(dir):\n",
    "        os.makedirs(dir)\n",
    "\n",
    "    if not os.path.isfile(filename):\n",
    "        print(\"Сохраняется \", filename)\n",
    "        with open(filename, \"w\") as f:\n",
    "            labels = list(map(\" \".join, np.eye(10, dtype=np.uint).astype(str)))\n",
    "            for row in ndarray:\n",
    "                row_str = row.astype(str)\n",
    "                label_str = labels[row[-1]]\n",
    "                feature_str = \" \".join(row_str[:-1])\n",
    "                f.write(\"|labels {} |features {}\\n\".format(label_str, feature_str))\n",
    "    else:\n",
    "        print(\"Такой файл уже существует\", filename)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_dir = os.path.join(\"..\", \"Image\", \"DataSets\", \"MNIST\")\n",
    "if not os.path.exists(data_dir):\n",
    "    data_dir = os.path.join(\"data\", \"MNIST\")\n",
    "\n",
    "print(\"Идёт запись файла...\")\n",
    "savetxt(os.path.join(data_dir, \"Traincntk.txt\"), train)\n",
    "\n",
    "print(\"Идёт запись файла...\")\n",
    "savetxt(os.path.join(data_dir, \"Testcntk.txt\"), test)\n",
    "\n",
    "print(\"Выполнено\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сохраним данные обучающей и тестовой выборок. У этого действа есть две причины, первая это банальное нежелание лишний раз загружать данные в блокноте при прогоне функционала CNTK. Вторая причина: при всей простоте использования данных в формате NumPy/SciPy/Pandas считывая их же reader-ами, это применимо к разве что небольшим наборам данных, т.к. глубокое обучение как правило требует больших объемов данных - стОит продемонстрировать работу встроенных CNTK-шных reader-ов, которые успешно масштабируются на случай терабайтов данных. Фомат файлов немного напоминает Vowpal Wabbit-овский:\n",
    "\n",
    "    |labels 0 0 0 1 0 0 0 0 0 0 |features 0 0 0 0 ... \n",
    "                                               (784 интов по одному на каждый пиксель)\n",
    "Ниже представлена функция \"create_reader\" для чтения сохранненых выше обучающей и тестовой выборок."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_reader(path, is_training, dimens, num_label_classes):\n",
    "\n",
    "    labelStream = cntk.io.StreamDef(\n",
    "        field=\"labels\", shape=num_label_classes, is_sparse=False\n",
    "    )\n",
    "    featureStream = cntk.io.StreamDef(field=\"features\", shape=dimens, is_sparse=False)\n",
    "\n",
    "    deserailizer = cntk.io.CTFDeserializer(\n",
    "        path, cntk.io.StreamDefs(labels=labelStream, features=featureStream)\n",
    "    )\n",
    "\n",
    "    return cntk.io.MinibatchSource(\n",
    "        deserailizer,\n",
    "        randomize=is_training,\n",
    "        max_sweeps=cntk.io.INFINITELY_REPEAT if is_training else 1,\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "data_found = False\n",
    "\n",
    "for data_dir in [\n",
    "    os.path.join(\"..\", \"Image\", \"DataSets\", \"MNIST\"),\n",
    "    os.path.join(\"data\", \"MNIST\"),\n",
    "]:\n",
    "    train_file = os.path.join(data_dir, \"Traincntk.txt\")\n",
    "    test_file = os.path.join(data_dir, \"Testcntk.txt\")\n",
    "    if os.path.isfile(train_file) and os.path.isfile(test_file):\n",
    "        data_found = True\n",
    "        break"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "### Построение модели\n",
    "Для классификации картинок будем использовать многоклассовую логистическую регрессию. <img src=\"../../img/NNmulticlassLR.png\">\n",
    "Число выходных узлов соответствует количеству значений, которое может принимать зависимая переменная. Каждый узел суммирования использует свой собственный набор весов для масштабирования входных функций и суммирования их вместе. Вместо того, чтобы передавать суммированный вывод взвешенных входных признаков через сигмоиду, выход пропускаем через [softmax][]-функцию, которая вдобвак к проецированию на отрезок [0;1] нормализует вывод выходных узлов.\n",
    "\n",
    "[softmax]: https://ru.wikipedia.org/wiki/Softmax"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Для воспроизводимости результатов\n",
    "np.random.seed(0)\n",
    "cntk.cntk_py.set_fixed_random_seed(17)\n",
    "cntk.cntk_py.force_deterministic_algorithms()\n",
    "\n",
    "num_classes = 10\n",
    "dimensions = 784"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Логистическа регрессия являет собой простую линейную модель, принимающую на вход вектор $\\bf\\vec{x}$ из чисел, описывающих классифицируемый объект и вычисляющую признаки($\\it\\vec{z}$). Для каждой из 10 цифр существует вектор весов, соответствующий входным значениям пикселей. Итого 10 значений, 784 пикселя - наша матрица весов $\\bf{W}$ имеет размерность 10x784. Конечно же не стОит забывать про вектор смещений $\\bf\\vec{b}$ длиной 10.\n",
    "Таким образом первым делом мы вычисляем: $\\it\\vec{z}=\\bf{W}\\bf{\\vec{x}}+\\bf{\\vec{b}}$ .\n",
    "Признаки $\\vec{z}$ еще не спроецированы на отрезок [0:1] сигмоидой, вместо этого результат нормализуется функцией [softmax][]: \n",
    "\n",
    "$$\\dfrac{{e}^{{z}_{i}}}{\\sum\\limits_{j=0}^9 {{e}^{{z}_{i}}}}$$\n",
    "\n",
    "и сумма пресказаний по всем классам становится равна 1. В CNTK при обучении будем использовать функцию, совмещающую **softmax** и функцию потерь **cross entropy error**.\n",
    "[softmax]: https://ru.wikipedia.org/wiki/Softmax"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Входная переменная (**input variable**) - является ключевым понятием CNTK. Под ней подразумевается контейнер, который мы наполняем некоторыми наблюдениями, в нашем случае пикселями картинки, во время обучения и тестирования. Таким образом, форма входной переменной должна соответствовать форме данных, которые будут предоставлены:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "iv = cntk.input_variable(dimensions)\n",
    "label = cntk.input_variable(num_classes)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Настройка нейронной сети для логистической регрессии\n",
    "В CNTK существует метод Dense, которая создает полносвязный слой выполняющий описанные выше операции взвешенного суммирования входных данных с добавлением смещения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def create_model(features):\n",
    "    with cntk.layers.default_options(init=cntk.glorot_uniform()):\n",
    "        r = cntk.layers.Dense(num_classes, activation=None)(features)\n",
    "        return r"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "будем использовать z для обозначения вывода нейронной сети:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Переведем из [0;255] значения пикселей в [0;1]\n",
    "input_s = iv / 255\n",
    "z = create_model(input_s)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Обучение\n",
    "Далее определим **функцию потерь**:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "loss = cntk.cross_entropy_with_softmax(z, label)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Зададим метрику для оценки качества модели: будем использовать функцию classification_error(), которая возвращает среднее среди сопосталенных экземпляров."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "le = cntk.classification_error(z, label)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Тюнинг\n",
    "Среди прочих, самым популярным способом уменьшить функцию потерь является стохастический градиентный спуск (SGD). Мы будем использовать mini-batch SGD - версию градиентного спуска, где на каждом шаге для обновления весов используется не один эземпляр, а несколько. Это дает преимущество, делая шаги менее зашумленными. При этом в процессе итерирования и обновления параметров модели, мы используем каждый раз новый пак образцов в нашем мини-батче. Как только инкрементные частоты ошибок перестают сильно меняться или после прохождения захардкоженного максимального числа минибатчей, используемых для обучения, мы говорим о натренированности модели."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "learning_rate = 0.2\n",
    "lr_schedule = cntk.learning_rate_schedule(learning_rate, cntk.UnitType.minibatch)\n",
    "learner = cntk.sgd(z.parameters, lr_schedule)\n",
    "trainer = cntk.Trainer(z, (loss, le), [learner])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def moving_average(a, w=5):\n",
    "    if len(a) < w:\n",
    "        return a[:]\n",
    "    return [\n",
    "        val if idx < w else sum(a[(idx - w) : idx]) / w for idx, val in enumerate(a)\n",
    "    ]\n",
    "\n",
    "\n",
    "def print_training_progress(trainer, mb, frequency, verbose=1):\n",
    "    training_loss = \"NA\"\n",
    "    eval_error = \"NA\"\n",
    "\n",
    "    if mb % frequency == 0:\n",
    "        training_loss = trainer.previous_minibatch_loss_average\n",
    "        eval_error = trainer.previous_minibatch_evaluation_average\n",
    "        if verbose:\n",
    "            print(\n",
    "                \"Минибатч: {0}, значение функции потерь: {1:.4f}, ошибка: {2:.2f}%\".format(\n",
    "                    mb, training_loss, eval_error * 100\n",
    "                )\n",
    "            )\n",
    "    return mb, training_loss, eval_error"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Запуск\n",
    "Довольно разговоров! Давайте, наконец, запустим эту сеть!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "minibatch_size = 64\n",
    "num_samples_per_run = 60000\n",
    "num_run_to_train_with = 20\n",
    "num_minibatches_to_train = (\n",
    "    num_samples_per_run * num_run_to_train_with\n",
    ") / minibatch_size"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Тот самый ридер, давно нами написанный, настал твой час\n",
    "reader_train = create_reader(train_file, True, dimensions, num_classes)\n",
    "\n",
    "# Отмапим потоки данных на входные переменные\n",
    "input_map = {label: reader_train.streams.labels, iv: reader_train.streams.features}\n",
    "\n",
    "# Обучим модель\n",
    "training_progress_output_freq = 500\n",
    "\n",
    "plotdata = {\"batchsize\": [], \"loss\": [], \"error\": []}\n",
    "\n",
    "for i in range(0, int(num_minibatches_to_train)):\n",
    "\n",
    "    # Прочитаем минибатч из обучающей выборки\n",
    "    data = reader_train.next_minibatch(minibatch_size, input_map=input_map)\n",
    "\n",
    "    trainer.train_minibatch(data)\n",
    "    batchsize, loss, error = print_training_progress(\n",
    "        trainer, i, training_progress_output_freq, verbose=1\n",
    "    )\n",
    "\n",
    "    if not (loss == \"NA\" or error == \"NA\"):\n",
    "        plotdata[\"batchsize\"].append(batchsize)\n",
    "        plotdata[\"loss\"].append(loss)\n",
    "        plotdata[\"error\"].append(error)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте выведем на график ошибки среди всех различных минибатчей. Как и положено, в процессе обучения значение функции потерь падает, хотя и не без выбросов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Вычислим скользящую среднюю значений функции потерь\n",
    "plotdata[\"avgloss\"] = moving_average(plotdata[\"loss\"])\n",
    "plotdata[\"avgerror\"] = moving_average(plotdata[\"error\"])\n",
    "\n",
    "# Построим графики функции потерь и ошибки\n",
    "plt.figure(1)\n",
    "plt.subplot(211)\n",
    "plt.plot(plotdata[\"batchsize\"], plotdata[\"avgloss\"], \"b--\")\n",
    "plt.xlabel(\"Число минибатчей\")\n",
    "plt.ylabel(\"Функция потерь\")\n",
    "plt.title(\"Функция потерь по мере обучения\")\n",
    "plt.show()\n",
    "\n",
    "plt.subplot(212)\n",
    "plt.plot(plotdata[\"batchsize\"], plotdata[\"avgerror\"], \"r--\")\n",
    "plt.xlabel(\"Число минибатчей\")\n",
    "plt.ylabel(\"Ошибка предсказания\")\n",
    "plt.title(\"Ошибка предсказания по мере обучения\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь давайте протестируем обученную нейронную сеть на тестовых данных. Это делается методом test_minibatch:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Прочтем тестовые данные\n",
    "reader_test = create_reader(test_file, False, dimensions, num_classes)\n",
    "\n",
    "test_input_map = {\n",
    "    label: reader_test.streams.labels,\n",
    "    iv: reader_test.streams.features,\n",
    "}\n",
    "\n",
    "# Параметры тестовых данных\n",
    "test_minibatch_size = 512\n",
    "num_samples = 10000\n",
    "num_minibatches_to_test = num_samples // test_minibatch_size\n",
    "test_result = 0.0\n",
    "\n",
    "for i in range(num_minibatches_to_test):\n",
    "\n",
    "    # Загружаем тестовую выборку минибатчами в соответствии с заданным test_minibatch_size\n",
    "    # Каждый кусочек данных в минибатче - картинка цифры из 784пикселей\n",
    "    data = reader_test.next_minibatch(test_minibatch_size, input_map=test_input_map)\n",
    "\n",
    "    eval_error = trainer.test_minibatch(data)\n",
    "    test_result = test_result + eval_error\n",
    "\n",
    "# Средняя ошибка на тестовой выборке по всем минибатчам\n",
    "print(\n",
    "    \"Средняя ошибка на тесте: {0:.2f}%\".format(\n",
    "        test_result * 100 / num_minibatches_to_test\n",
    "    )\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "До сих пор мы имели дело с совокупными мерами ошибки. Возьмем теперь вероятности, связанные с отдельными точками данных. Для каждого наблюдения функция eval возвращает распределение вероятности по всем классам. Всего классов по количеству цифр - 10. Сначала проложим выход через функцию softmax. Это сопоставит агрегированные активации по сети с вероятностями по 10 классам. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "out = cntk.softmax(z)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте протестируем на маленьком батче данных из тестовой выборки:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Считываем тестовые данные\n",
    "reader_eval = create_reader(test_file, False, dimensions, num_classes)\n",
    "\n",
    "eval_minibatch_size = 25\n",
    "eval_input_map = {iv: reader_eval.streams.features}\n",
    "\n",
    "data = reader_test.next_minibatch(eval_minibatch_size, input_map=test_input_map)\n",
    "\n",
    "img_label = data[label].asarray()\n",
    "img_data = data[iv].asarray()\n",
    "predicted_label_prob = [out.eval(img_data[i]) for i in range(len(img_data))]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Найдем индекс с максимальным значением предсказанной вероятности\n",
    "pred = [np.argmax(predicted_label_prob[i]) for i in range(len(predicted_label_prob))]\n",
    "gtlabel = [np.argmax(img_label[i]) for i in range(len(img_label))]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Метка по факту:\", gtlabel[:25])\n",
    "print(\"Предсказание  :\", pred)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как можно убедиться, модель не идеальна, но свою задачу наглядной демонстрации работы вполне выполняет.\n",
    "\n",
    "Литературу и ресурсы, для дальнейшего ознакомления можно найти по ссылкам:\n",
    "1. https://github.com/Microsoft/CNTK\n",
    "2. https://docs.microsoft.com/en-us/cognitive-toolkit/\n",
    "3. https://www.microsoft.com/en-us/research/publication/1-bit-stochastic-gradient-descent-and-application-to-data-parallel-distributed-training-of-speech-dnns/"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
